package com.itstyle.seckill.service.impl;

import com.itstyle.seckill.common.aop.ServiceLimit;
import com.itstyle.seckill.common.aop.ServiceLock;
import com.itstyle.seckill.common.dynamicquery.DynamicQuery;
import com.itstyle.seckill.common.entity.Result;
import com.itstyle.seckill.common.entity.Seckill;
import com.itstyle.seckill.common.entity.SuccessKilled;
import com.itstyle.seckill.common.enums.SeckillStatEnum;
import com.itstyle.seckill.common.exception.RrException;
import com.itstyle.seckill.repository.SeckillRepository;
import com.itstyle.seckill.service.ISeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Timestamp;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service("seckillService")
public class SeckillServiceImpl implements ISeckillService {

    /**
     * 思考：为什么不用  synchronized
     * service 默认是单例的，并发下 lock 只有一个实例
     * 互斥锁 参数默认 false，不公平锁
     */
    private Lock lock = new ReentrantLock(true);

    @Autowired
    private DynamicQuery dynamicQuery;
    @Autowired
    private SeckillRepository seckillRepository;

    @Override
    public List<Seckill> getSeckillList() {
        return seckillRepository.findAll();
    }

    @Override
    public Seckill getById(long seckillId) {
        return seckillRepository.findOne(seckillId);
    }

    @Override
    public Long getSeckillCount(long seckillId) {
        String nativeSql = "SELECT count(*) FROM success_killed WHERE seckill_id=?";
        Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
        return ((Number) object).longValue();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteSeckill(long seckillId) {
        String nativeSql = "DELETE FROM  success_killed WHERE seckill_id=?";
        dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
        nativeSql = "UPDATE seckill SET number =100 WHERE seckill_id=?";
        dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckill(long seckillId, long userId) {
        // 校验库存
        String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
        Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
        Long number = ((Number) object).longValue();
        if (number > 0) {
            // 扣库存
            nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
            dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
            // 创建订单
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(seckillId);
            killed.setUserId(userId);
            killed.setState((short) 0);
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            dynamicQuery.save(killed);
            /**
             * 这里仅仅是分表而已，提供一种思路，供参考，测试的时候自行建表
             * 按照用户 ID 来做 hash 分散订单数据。
             * 要扩容的时候，为了减少迁移的数据量，一般扩容是以倍数的形式增加。
             * 比如原来是 8 个库，扩容的时候，就要增加到 16 个库，再次扩容，就增加到 32 个库。
             * 这样迁移的数据量，就小很多了。
             * 这个问题不算很大问题，毕竟一次扩容，可以保证比较长的时间，而且使用倍数增加的方式，已经减少了数据迁移量。
             */
            String table = "success_killed_" + userId % 8;
            nativeSql = "INSERT INTO " + table + " (seckill_id, user_id,state,create_time)VALUES(?,?,?,?)";
            Object[] params = new Object[]{seckillId, userId, (short) 0, new Timestamp(System.currentTimeMillis())};
            dynamicQuery.nativeExecuteUpdate(nativeSql, params);
            //支付
            return Result.ok(SeckillStatEnum.SUCCESS);
        } else {
            return Result.error(SeckillStatEnum.END);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckillLock(long seckillId, long userId) {
        lock.lock();
        try {
            /**
             * 1)这里、不清楚为啥、总是会被超卖 101、难道锁不起作用、lock 是同一个对象
             * 2)事务未提交之前，锁已经释放（事务提交是在整个方法执行完），导致另一个事务读取到了这个事务未提交的数据，也就是传说中的脏读。建议锁上移
             * 3)给自己留个坑思考：为什么分布式锁（zk 和 redis）没有问题？（事实是有问题的，由于 redis 释放锁需要远程通信，不那么明显而已）
             * 4)数据库默认的事务隔离级别为 可重复读（repeatable-read），也就不可能出现脏读，幻读侧重于新增或删除，这里显然不是，那这里到底是什么，给各位大婶留个坑~~~~
             */
            String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
            Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
            Long number = ((Number) object).longValue();
            if (number > 0) {
                nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
                dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(seckillId);
                killed.setUserId(userId);
                killed.setState(Short.parseShort(number + ""));
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                dynamicQuery.save(killed);
            } else {
                return Result.error(SeckillStatEnum.END);
            }
        } catch (Exception e) {
            throw new RrException("异常了个乖乖");
        } finally {
            lock.unlock();
        }
        return Result.ok(SeckillStatEnum.SUCCESS);
    }

    @Override
    @ServiceLock
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckillAopLock(long seckillId, long userId) {
        // 使用 AOP + 锁实现
        // 事务切面的 default Order 被设置为了 Ordered.LOWEST_PRECEDENCE，所以默认情况下是属于最内层的环切
        String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
        Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
        Long number = ((Number) object).longValue();
        if (number > 0) {
            nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
            dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(seckillId);
            killed.setUserId(userId);
            killed.setState(Short.parseShort(number + ""));
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            dynamicQuery.save(killed);
        } else {
            return Result.error(SeckillStatEnum.END);
        }
        return Result.ok(SeckillStatEnum.SUCCESS);
    }

    // 注意这里限流注解可能会出现少买，自行调整
    @Override
    @ServiceLimit(limitType = ServiceLimit.LimitType.IP)
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckillDBPCC_ONE(long seckillId, long userId) {
        // 单用户抢购一件商品或者多件都没有问题
        String nativeSql = "SELECT number FROM seckill WHERE seckill_id=? FOR UPDATE";
        Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
        Long number = ((Number) object).longValue();
        if (number > 0) {
            nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
            dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(seckillId);
            killed.setUserId(userId);
            killed.setState((short) 0);
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            dynamicQuery.save(killed);
            return Result.ok(SeckillStatEnum.SUCCESS);
        } else {
            return Result.error(SeckillStatEnum.END);
        }
    }

    /**
     * SHOW STATUS LIKE 'innodb_row_lock%';
     * 如果发现锁争用比较严重，如 InnoDB_row_lock_waits 和 InnoDB_row_lock_time_avg 的值比较高
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckillDBPCC_TWO(long seckillId, long userId) {
        /**
         * 单用户抢购一件商品没有问题、但是抢购多件商品不建议这种写法 UPDATE 锁表
         * 
         * 乐观锁（ Optimistic Locking） 相对悲观锁而言，乐观锁假设认为数据一般情况下不会造成冲突，所以在数据进行提交更新的时候，才会正式对数据的冲突与否进行检测 ，如果发现冲突了，则让返回用户错误的信息，让用户决定如何去做。
         *
         * 悲观锁，正如其名，它指的是对数据被外界（包括本系统当前的其他事务，以及来自外部系统的事务处理）修改持保守态度（悲观），因此，在整个数据处理过程中，将数据处于锁定状态。
         *
         * 简单来说：
         * 悲观锁应该是全过程锁，乐观锁是 UPDATE 时锁。
         *
         * 对于
         * 悲观锁1：通过 for update 加上了排它锁，将其他事务将不再能进行读 or 写操作，明显的悲观操作。
         *
         * 悲观锁2：UPDATE 的 WHERE 条件是 先查询的id，也就是主键索引，只会走 InnoDB 的行级锁，并不会因为 num > 0 这个非索引条件去锁表。
         * 当这个 UPDATE 事务未提交的时候，其他事务依然是可以执行读操作的，所以并没有排它锁，仅在 UPDATE 操作上加了行锁，这一点和乐观锁 UPDATE version 的锁是一样的。
         */
        // UPDATE 更新语句本身就是锁表（行）数据
        String nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=? AND number>0";
        int count = dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
        if (count > 0) {
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(seckillId);
            killed.setUserId(userId);
            killed.setState((short) 0);
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            dynamicQuery.save(killed);
            return Result.ok(SeckillStatEnum.SUCCESS);
        } else {
            return Result.error(SeckillStatEnum.END);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result startSeckillDBOCC(long seckillId, long userId, long number) {
        Seckill kill = seckillRepository.findOne(seckillId);
        /**
         * 剩余的数量应该要大于等于秒杀的数量
         */
        if (kill.getNumber() >= number) {
            // 乐观锁
            String nativeSql = "UPDATE seckill  SET number=number-?,version=version+1 WHERE seckill_id=? AND version = ?";
            int count = dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{number, seckillId, kill.getVersion()});
            if (count > 0) {
                SuccessKilled killed = new SuccessKilled();
                killed.setSeckillId(seckillId);
                killed.setUserId(userId);
                killed.setState((short) 0);
                killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
                dynamicQuery.save(killed);
                return Result.ok(SeckillStatEnum.SUCCESS);
            } else {
                return Result.error(SeckillStatEnum.END);
            }
        } else {
            return Result.error(SeckillStatEnum.END);
        }
    }

    @Override
    public Result startSeckillTemplate(long seckillId, long userId, long number) {
        return null;
    }

}
